package blockworld;

import java.awt.Point;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import connexion.EnterMessage;
import connexion.IncomingMessageReader;
import connexion.IntegerMessage;
import connexion.Message;
import connexion.PerceptionMessage;
import connexion.PlacementMessage;
import connexion.StringMessage;
import connexion.StaticParameters;

public class Client 
{
	private static String teamName = "<undefined>";
	private static int width = 0;
	private static int height = 0;
	private static int nrAgents = 0;
	private static int placement = 0;
	private final static int shootingDistance = 5;
	private final static int serverPort = 2004;
	
	
	private static long movePhaseStarted;
	private static long shootPhaseStarted;
	private static volatile List<String> agentsAlive = new ArrayList<String>();
	private static volatile int agentsActed = 0;
	private static volatile int state = GameState.INITIALISING;
	
	static ObjectOutputStream out;
	//static ObjectInputStream in;
	static IncomingMessageReader imr;
	
	
	
	
	private static List<MoveAction> movements = new ArrayList<MoveAction>();
	
	private static Env environment;
	
	//Set<String> agents = new HashSet<String>();
	
	//static constructor?
	static
	{
		try
		{
			initNetwork();
			startProtocol();
		}
		catch(Exception e)
		{
			e.printStackTrace();
			System.out.println("Failed to setup Client!");
		}
	}
	
	public static void setEnv(Env env)
	{
		environment = env;
	}
	
	public static String getTeamName()
	{
		return teamName;
	}
	
	public static int getWidth()
	{
		return width;
	}
	
	public static int getHeight()
	{
		return height;
	}
	
	public static int getNrAgents()
	{
		return nrAgents;
	}
	
	public static int getPlacement()
	{
		return placement;
	}
	
	public static int shootingDistance()
	{
		return shootingDistance;
	}
	
	public synchronized static void enter(String agentName, int bodyType, int x, int y) 
	{
		if(state !=GameState.PLACEMENT)
			throw new Error();
		
		try
		{
			sendMessage( new EnterMessage
					(
				agentName,
				teamName,
				EnterMessage.PLACEMENT,
				bodyType,
				new Point(x,y))
			);
			agentsActed++;
			
			agentsAlive.add(agentName);
			
			if(agentsActed == nrAgents)
			{
				allAgentsEntered();
			}
			
		}
		catch(Exception e)
		{
			e.printStackTrace();
			System.exit(-1);
		}
		
		
	}
	
	public synchronized static void move(String agentName, Point from, int direction)
	{
		if(state != GameState.MOVEMENT)
		 throw new Error();
		
		if(!agentsAlive.contains(agentName))
			throw new Error();
		
		movements.add(new MoveAction(from, direction, agentName));
		
		if(StaticParameters.CLIENT_OUTGOING_MESSAGE_VERBOSITY)
			System.out.println("preparing move selection");
		try
		{
			agentsActed++;
			
			if(agentsActed == agentsAlive.size())
			{
				allAgentsMoved();
			}
		}
		catch(Exception e)
		{
			e.printStackTrace();
			System.exit(-1);
		}
		
		if(StaticParameters.CLIENT_OUTGOING_MESSAGE_VERBOSITY)
			System.out.println("move selection done");
		
	}
	
	public synchronized static void noMove(String agentName)
	{
		if(state != GameState.MOVEMENT)
			 throw new Error();
		
		//if(!agentsAlive.contains(agentName))
		//	throw new Error();
		
		try
		{
		
			agentsActed++;
		
			if(agentsActed == agentsAlive.size())
			{
				allAgentsMoved();
			}
		}
		catch(Exception e)
		{
			e.printStackTrace();
			System.exit(-1);
		}
	}
	
	public synchronized static void shoot(String agentName, int x, int y)
	{
		if(state != GameState.SHOOTING)
			throw new Error();
		
		//if(!agentsAlive.contains(agentName))
		//	throw new Error();

		if(StaticParameters.CLIENT_OUTGOING_MESSAGE_VERBOSITY)
			System.out.println("preparing shoot message");
		
		try
		{
			sendMessage(new PlacementMessage
			(
				agentName,
				teamName,
				PlacementMessage.SHOOTING_REASON,
				new Point(x,y)
			));
			
			agentsActed++;
			
			if(agentsActed == agentsAlive.size())
			{
				allAgentsShooted();
			}
		}
		catch(Exception e)
		{
			e.printStackTrace();
			System.exit(-1);
		}
		
		if(StaticParameters.CLIENT_OUTGOING_MESSAGE_VERBOSITY)
			System.out.println("move selection done");
	}
	
	public synchronized static void noShoot(String agentName)
	{
		if(state != GameState.SHOOTING)
			throw new Error();
		
		if(!agentsAlive.contains(agentName))
			throw new Error();
		
		try
		{
			agentsActed++;
		
			if(agentsActed == agentsAlive.size())
			{
				allAgentsShooted();
			}
		}
		catch(Exception e)
		{
			e.printStackTrace();
			System.exit(-1);
		}
	}
	
	private static void initNetwork() throws UnknownHostException, IOException 
	{
		Socket requestSocket = new Socket("localhost", serverPort);

		// 2. get Input and Output streams
		out = new ObjectOutputStream(requestSocket.getOutputStream());
		out.flush();
		imr=new IncomingMessageReader(
				new ObjectInputStream(requestSocket.getInputStream()));
	}
	
	private static void startProtocol() throws ClassNotFoundException, IOException 
	{
		readProperties();
		
		state = GameState.PLACEMENT;
	}
	
	private synchronized static void allAgentsEntered() throws IOException, ClassNotFoundException 
	{
		agentsActed = 0;
		readPlacementFinishedMessage();
		readGameStartedMessage();
		if(environment != null)
		{
			String[] agents = new String[agentsAlive.size()];
			agents = agentsAlive.toArray(agents);
			environment.gameStarted(agents);
		}
		state = GameState.MOVEMENT;
		movePhaseStarted = System.nanoTime();
	}
	
	private synchronized static void allAgentsMoved() throws IOException, ClassNotFoundException
	{
		System.out.println("------- TOOK " + ((System.nanoTime() - movePhaseStarted) / 1000000) + " Seconds for registerinMove." );
		
		List<MoveAction> finalMovementsFirst = new ArrayList<MoveAction>();
		List<MoveAction> finalMovementsSecond = new ArrayList<MoveAction>();
		
		for(MoveAction movement : movements)
		{
			boolean isBlocked = false;
			for(MoveAction movement2 : movements)
			{
				if(movement.getTo().equals(movement2.getFrom()))
				{
					isBlocked = true;
				}
			}
			
			if(!isBlocked)
			{
				finalMovementsFirst.add(movement);
			}
			else
			{
				finalMovementsSecond.add(movement);
			}
		}
		
		movements.clear();
		
		for(MoveAction moveaction : finalMovementsFirst)
		{
			System.out.println("MOVING FIRST " + moveaction.getAgentName() + " " + moveaction.getFrom());
			
			if(StaticParameters.CLIENT_OUTGOING_MESSAGE_VERBOSITY)
				System.out.println("preparing move selection");
			try
			{
				sendMessage(new IntegerMessage(
						IntegerMessage.DIRECTION,
						moveaction.getAgentName(),
						teamName,
						moveaction.getDirection())
				);
			}
			catch(Exception e)
			{
				e.printStackTrace();
				System.exit(-1);
			}
		}
		
		for(MoveAction moveaction : finalMovementsSecond)
		{
			System.out.println("MOVING SECOND " + moveaction.getAgentName() + " " + moveaction.getFrom());
			
			if(StaticParameters.CLIENT_OUTGOING_MESSAGE_VERBOSITY)
				System.out.println("preparing move selection");
			try
			{
				sendMessage(new IntegerMessage(
						IntegerMessage.DIRECTION,
						moveaction.getAgentName(),
						teamName,
						moveaction.getDirection())
				);
			}
			catch(Exception e)
			{
				e.printStackTrace();
				System.exit(-1);
			}
		}
		
		System.out.println("------- TOOK " + ((System.nanoTime() - movePhaseStarted) / 1000000) + " miliSeconds for registerinMove." );
		
		agentsActed = 0;
		readMoveOutcomeMessage();
		
		System.out.println("------- MOVE PHASE FINISHED total time:" + ((System.nanoTime() - movePhaseStarted) / 1000000) + "-----------");
		//environment.printWorlds();
	}
	
	private synchronized static void allAgentsShooted() throws IOException, ClassNotFoundException
	{
		System.out.println("------- TOOK " + ( (System.nanoTime() - shootPhaseStarted) / 1000000) + " miliSeconds for registerinShoot." );
		
		agentsActed = 0;
		readShotsOutcomeMessage();
		
		System.out.println("------- SHOOT PHASE FINISHED total time:" + ( (System.nanoTime() - shootPhaseStarted) / 1000000) + "-----------");
		//environment.printWorlds();
		
		
	}
	
	private static Object readObject() throws IOException, ClassNotFoundException 
	{
		Object o= imr.readFirst();
		if(StaticParameters.CLIENT_INCOMING_MESSAGE_VERBOSITY)
			System.out.println("client<"+o);
		return o;
		
	}
	
	public static void sendMessage(Message m) throws IOException 
	{
		if(StaticParameters.CLIENT_OUTGOING_MESSAGE_VERBOSITY)
			System.out.println("client>"+m);
		
		out.writeObject(m);
	}
	
	private static void readProperties() throws IOException, ClassNotFoundException
	{
		teamName = readNameMessage();
		width = readWidthMessage();
		height = readHeightMessage();
		nrAgents = readNrAgentsMessage();
		placement = readPlacementMessage();
		
	}
	
	private static String readNameMessage() throws IOException, ClassNotFoundException 
	{
		StringMessage m=(StringMessage)readObject();
		
		//if(m.getReason().equals(StringMessage.NAMING))
			return m.getValue();
		//else
		//	throw new Error();
	}
	
	private static int readHeightMessage() throws IOException, ClassNotFoundException 
	{
		IntegerMessage im=(IntegerMessage) readObject();
		
		if(im.getReason().equals(IntegerMessage.HEIGHT_REASON))
			return im.getValue();
		else 
			throw new Error();
	}

	private static int readWidthMessage() throws IOException, ClassNotFoundException 
	{
		IntegerMessage im=(IntegerMessage) readObject();
		
		if(im.getReason().equals(IntegerMessage.WIDTH_REASON))
			return im.getValue();
		else 
			throw new Error();
	}
	
	private static int readNrAgentsMessage() throws IOException, ClassNotFoundException 
	{
		IntegerMessage im=(IntegerMessage) readObject();
		
		if(im.getReason().equals(IntegerMessage.AGENT_NUMBER))
			return im.getValue();
		else 
			throw new Error();
	}
	
	private static int readPlacementMessage() throws IOException, ClassNotFoundException 
	{
		IntegerMessage im=(IntegerMessage) readObject();
		
		if(im.getReason().equals(IntegerMessage.PLACEMENT_REASON))
			return im.getValue();
		else 
			throw new Error();
	}
		
	private static void readPlacementFinishedMessage() throws IOException, ClassNotFoundException 
	{
		StringMessage s=(StringMessage)readObject();
		
		if(!s.getReason().equals(StringMessage.PLACEMENT_FINISHED))
			throw new Error();
	}
	
	private static void readGameStartedMessage() throws IOException, ClassNotFoundException 
	{
		StringMessage s=(StringMessage)readObject();
		if(!s.getReason().equals(StringMessage.GAME_STARTED))
			throw new Error();
	}
	
	private static void readShotsOutcomeMessage() throws IOException, ClassNotFoundException {
		sense(StringMessage.SHOOT_PHASE_FINISHED);
	}
	
	private static void readMoveOutcomeMessage() throws IOException, ClassNotFoundException {
		sense(StringMessage.MOVE_PHASE_FINISHED);
	}
	
	private static void sense(String senseTermination) throws IOException, ClassNotFoundException
	{
		boolean moveOutcomeFinished=false;
		
		List<PerceptionMessage> perceptionMessages = new ArrayList<PerceptionMessage>();
		List<String> agentsSensedFor = new ArrayList<String>();
		
	
		while(!moveOutcomeFinished)
		{
			Object o=readObject();
			if(o instanceof StringMessage && ((StringMessage)o).getValue().equals(senseTermination))
				moveOutcomeFinished=true;
			else if(o instanceof PerceptionMessage)
			{
				PerceptionMessage pm=(PerceptionMessage)o;
				perceptionMessages.add(pm);
			}
		}
		
		String stateForAgent = null;
		//change state before sending sense.
		if(senseTermination.equals(StringMessage.MOVE_PHASE_FINISHED))
		{
			shootPhaseStarted = System.nanoTime();
			state = GameState.SHOOTING;
			stateForAgent = "afterMove";
		}
		else if(senseTermination.equals(StringMessage.SHOOT_PHASE_FINISHED))
		{
			movePhaseStarted = System.nanoTime();
			state = GameState.MOVEMENT;
			stateForAgent ="afterShoot"; 
		}
		
		
		
		
		System.out.println("******** SENSE INFORMATION begin ******");
		for(PerceptionMessage pm : perceptionMessages)
		{
			if(environment != null)
			{
				environment.sensed(stateForAgent,pm.getAgent(), pm.getPosition(), pm.getEntities());
				agentsSensedFor.add(pm.getAgent());
			}
		}
		for(String agentname : agentsAlive)
		{
			if(agentsSensedFor.contains(agentname))
			{
				//agent is alive thank god.
			}
			else
			{
				if(environment != null)
				{
					environment.died(agentname);
				}
			}
		}
		
		agentsAlive = agentsSensedFor;		
		
	}
	
	//deprecated
	public static String getState()
	{
		switch( state )
		{
			case GameState.MOVEMENT:
				return "move";
			case GameState.SHOOTING:
				return "shoot";
			default:
				return "";
		}
	}
	
}
